import { Architecture, getOSType, OSType } from "../../../../common/utils/platform";
import { traceError, traceWarn } from "../../../../logging";
import { getEnvironmentDirFromPath, getEQVersionFromPath, getInterpreterPathFromDir } from "../../../common/commonUtils";
import { AnacondaCompanyName, Conda } from "../../../common/environmentManagers/conda";
import { EQEnvInfo, EQEnvKind, EQEnvSource, UNKNOWN_EQ_VERSION, virtualEnvKinds } from "../../info";
import { areSameEnv, buildEnvInfo, compareEQVersionSpecificity, getEnvID, setEnvDisplayString } from "../../info/env";
import { BasicEnvInfo } from "../../locator";
import { uniq } from 'lodash';
import { Uri } from "vscode";
import { arePathsSame, getWorkspaceFolders, isParentPath } from "../../../common/externalDependencies";
import * as path from 'path';
import { parseVersion } from "../../info/eqVersion";
import { getRegistryInterpreters, getRegistryInterpretersSync } from "../../../common/windowsUtils";
import { parseVersionFromExecutable } from "../../info/executable";

function getResolvers(): Map<EQEnvKind, (env: BasicEnvInfo, useCache?: boolean) => Promise<EQEnvInfo>> {
    const resolvers = new Map<EQEnvKind, (_: BasicEnvInfo, useCache?: boolean) => Promise<EQEnvInfo>>();
    Object.values(EQEnvKind).forEach((k) => {
        resolvers.set(k, resolveGloballyInstalledEnv);
    });
    virtualEnvKinds.forEach((k) => {
        resolvers.set(k, resolveSimpleEnv);
    });
    resolvers.set(EQEnvKind.Conda, resolveCondaEnv);
    return resolvers;
}


function getSearchLocation(env: EQEnvInfo): Uri | undefined {
    const folders = getWorkspaceFolders();
    const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f));
    if (isRootedEnv) {
        // For environments inside roots, we need to set search location so they can be queried accordingly.
        // Search location particularly for virtual environments is intended as the directory in which the
        // environment was found in.
        // For eg.the default search location for an env containing 'bin' or 'Scripts' directory is:
        //
        // searchLocation <--- Default search location directory
        // |__ env
        //    |__ bin or Scripts
        //        |__ eq  <--- executable
        return Uri.file(path.dirname(env.location));
    }
    return undefined;
}


async function updateEnvUsingRegistry(env: EQEnvInfo): Promise<void> {
    // Environment source has already been identified as windows registry, so we expect windows registry
    // cache to already be populated. Call sync function which relies on cache.
    let interpreters = getRegistryInterpretersSync();
    if (!interpreters) {
        traceError('Expected registry interpreter cache to be initialized already');
        interpreters = await getRegistryInterpreters();
    }
    const data = interpreters.find((i) => arePathsSame(i.interpreterPath, env.executable.filename));
    if (data) {
        const versionStr = data.versionStr ?? data.sysVersionStr ?? data.interpreterPath;
        let version;
        try {
            version = parseVersion(versionStr);
        } catch (ex) {
            version = UNKNOWN_EQ_VERSION;
        }
        env.kind = env.kind === EQEnvKind.Unknown ? EQEnvKind.OtherGlobal : env.kind;
        env.version = compareEQVersionSpecificity(version, env.version) > 0 ? version : env.version;
        env.distro.defaultDisplayName = data.companyDisplayName;
        env.arch = data.bitnessStr === '32bit' ? Architecture.x86 : Architecture.x64;
        env.distro.org = data.distroOrgName ?? env.distro.org;
        env.source = uniq(env.source.concat(EQEnvSource.WindowsRegistry));
    } else {
        traceWarn('Expected registry to find the interpreter as source was set');
    }
}

export async function resolveBasicEnv(env: BasicEnvInfo, useCache = false): Promise<EQEnvInfo> {
    const { kind, source } = env;
    const resolvers = getResolvers();
    const resolverForKind = resolvers.get(kind)!;
    const resolvedEnv = await resolverForKind(env, useCache);
    resolvedEnv.searchLocation = getSearchLocation(resolvedEnv);
    resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? []));
    if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(EQEnvSource.WindowsRegistry)) {
        // We can update env further using information we can get from the Windows registry.
        await updateEnvUsingRegistry(resolvedEnv);
    }
    setEnvDisplayString(resolvedEnv);
    resolvedEnv.id = getEnvID(resolvedEnv.executable.filename, resolvedEnv.location);
    return resolvedEnv;
}

async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise<EQEnvInfo> {
    const { executablePath } = env;
    const conda = await Conda.getConda();
    if (conda === undefined) {
        traceWarn(`${executablePath} identified as Conda environment even though Conda is not installed`);
    }
    const envs = (await conda?.getEnvList(useCache)) ?? [];
    for (const { name, prefix } of envs) {
        let executable = await getInterpreterPathFromDir(prefix);
        const currEnv: BasicEnvInfo = { executablePath: executable ?? '', kind: EQEnvKind.Conda, envPath: prefix };
        if (areSameEnv(env, currEnv)) {
            if (env.executablePath.length > 0) {
                executable = env.executablePath;
            } else {
                executable = await conda?.getInterpreterPathForEnvironment({ name, prefix });
            }
            const info = buildEnvInfo({
                executable,
                kind: EQEnvKind.Conda,
                org: AnacondaCompanyName,
                location: prefix,
                source: [],
                version: executable ? await getEQVersionFromPath(executable) : undefined,
            });
            if (name) {
                info.name = name;
            }
            return info;
        }
    }
    traceError(
        `${env.envPath ?? env.executablePath} identified as a Conda environment but is not returned via '${conda?.command
        } info' command`,
    );
    // Environment could still be valid, resolve as a simple env.
    env.kind = EQEnvKind.Unknown;
    return resolveSimpleEnv(env);
}

async function resolveSimpleEnv(env: BasicEnvInfo): Promise<EQEnvInfo> {
    const { executablePath, kind } = env;
    const envInfo = buildEnvInfo({
        kind,
        version: await getEQVersionFromPath(executablePath),
        executable: executablePath,
    });
    const location = getEnvironmentDirFromPath(executablePath);
    envInfo.location = location;
    envInfo.name = path.basename(location);
    return envInfo;
}

async function resolveGloballyInstalledEnv(env: BasicEnvInfo): Promise<EQEnvInfo> {
    const { executablePath } = env;
    let version;
    try {
        version = parseVersionFromExecutable(executablePath);
    } catch {
        version = UNKNOWN_EQ_VERSION;
    }
    const envInfo = buildEnvInfo({
        kind: env.kind,
        version,
        executable: executablePath,
    });
    return envInfo;
}